classdef dotsTheMachineConfiguration < dotsAllSingletonObjects
    % @class dotsTheMachineConfiguration
    % Singleton to organize permanent, machine-specific settings
    % @details
    % dotsTheMachineConfiguration organizes values for configuring a
    % particular computer, such as its IP address, screen dimensions, or
    % a system path to locate image files.  These are values a user might
    % set once and then forget about.  They probably should change only
    % when the confuguration of the machine itself changes.
    % @details
    % For settings that a user might want to change from day to day, or
    % even during an experiment, see dotsTheSwitchboard.  The two work
    % together, in that dotsTheMachineConfiguration sets some
    % machine-specific "sharedDefaults" used by dotsTheSwitchboard.
    % @details
    % Each setting value belongs to a group, which is usually a class
    % name.  Each setting also has a string mnemonic, which is usually the
    % name of a class property.  Thus, the method
    % dotsTheMachineConfiguration.applySettingsToObject() provides a
    % way to set machine-specific defaults for object properties.
    % @details
    % dotsTheMachineConfiguration starts out with a set of default machine
    % settings which are valid but not always useful.  From there it can
    % read and write custom settings from and to an .xml file.  Indeed, if
    % it finds a file on Matlab's path with a particular name it will load
    % settings from that file automatically.  See getHostSettingsFilename()
    % for the machine-specific value of this "host settings" file.
    % @details
    % The easiest way to make a local settings file is to call
    % dotsTheMachineConfiguration.writeUserSettingsFile().  This
    % will open a dialog for saving an settings .xml file.  The default
    % filename comes from getHostSettingsFilename(), so it's suitable for
    % automatic loading.
    % @details
    % Users can load any other .xml file with
    % dotsTheMachineConfiguration.readUserSettingsFile().
    % @details
    % The .xml files store values as "evalable" strings.  That is, each
    % string is passed to Matlab's built-in eval() function and the
    % result is used as the settings value.  This allows values to be
    % human-readable and also very flexible.
    % @details
    % The names of settings groups and mnemonics must all be valid xml
    % names, which are generally letters with no symbols or spaces.
    %
    % @ingroup foundation
    
    properties
        % topsGroupedList that contains the current settings values
        % @details
        % Settings groups are string names, usually names of classes.
        % Setting mnemonics are also strings, usually names of class
        % properties.
        settingsList;
        
        % file name of the currently loaded settings file
        % @details
        % settingsFile may contain an absolute path along with the name of
        % a settings .xml file.  If settingsFile is empty or Matlab cannot
        % locate it with the built-in exist() function,
        % dotsTheMachineConfiguration will use default settings values.
        settingsFile;
    end
    
    methods (Access = private)
        % Constructor is private.
        % @details
        % dotsTheMachineConfiguration is a singleton object, so its
        % constructor is not accessible.  Use
        % dotsTheMachineConfiguration.theObject() to access the current
        % instance.
        function self = dotsTheMachineConfiguration(varargin)
            self.set(varargin{:});
            self.initialize;
        end
    end
    
    methods
        % Restore dotsTheMachineConfiguration to a fresh state.
        % @details
        % Discards the current settings values and attempts to reload
        % values from one of the following sources, in order:
        %   - the current settingsFile
        %   - the default "host settings" file
        %   - the hard-coded defaults
        function initialize(self)
            self.initializeLists({'settingsList'});
            
            %             keyboard;
            hostFile = dotsTheMachineConfiguration.getHostSettingsFilename;
            if exist(self.settingsFile, 'file')
                % reload current settings
                self.settingsFromXmlFile(self.settingsFile);
                
            elseif exist(hostFile, 'file')
                % load "host settings"
                self.settingsFromXmlFile(which(hostFile));
                
            else
                % fallback on generic defaults
                self.setDefaultConfiguration;
                self.settingsFile = '';
            end
        end
        
        % Restore dotsTheMachineConfiguration to its factory state.
        % @details
        % Discards the current settings values and loads the hard-coded
        % factory defaults.
        function setDefaultConfiguration(self)
            self.settingsList.removeAllGroups;
            
            group = 'dotsTheSwitchboard';
            sharedDefaults = struct( ...
                'isVisible', true, ...
                'isAudible', true, ...
                'imagesPath', '~', ...
                'soundsPath', '~', ...
                'tasksPath', '~', ...
                'dataPath', '~', ...
                'clockFunction', @topsClock, ...
                'waitFevalable', {{@pause, .001}}, ...
                'backgroundColor', [0 0 0], ...
                'foregroundColor', [1 1 1]*255, ...
                'socketClassname', 'dotsSocketPnet', ...
                'dOutClassname', 'dotsDOut1208FS');
            self.settingsList.addItemToGroupWithMnemonic(sharedDefaults, group, 'sharedDefaults');
            
            group = 'dotsPlayable';
            self.settingsList.addItemToGroupWithMnemonic(44100, group, 'sampleFrequency');
            self.settingsList.addItemToGroupWithMnemonic(16, group, 'bitsPerSample');
            
            group = 'dotsTheScreen';
            self.settingsList.addItemToGroupWithMnemonic(0, group, 'displayIndex');
            self.settingsList.addItemToGroupWithMnemonic(1, group, 'width');
            self.settingsList.addItemToGroupWithMnemonic(1, group, 'height');
            self.settingsList.addItemToGroupWithMnemonic(1, group, 'distance');
            self.settingsList.addItemToGroupWithMnemonic([], group, 'bitDepth');
            self.settingsList.addItemToGroupWithMnemonic(1, group, 'multisample');
            
            group = 'dotsTheMessenger';
            self.settingsList.addItemToGroupWithMnemonic(.1, group, 'messageTimeout');
            self.settingsList.addItemToGroupWithMnemonic(.01, group, 'resendInterval');
            self.settingsList.addItemToGroupWithMnemonic('127.0.0.1', group, 'defaultClientIP');
            self.settingsList.addItemToGroupWithMnemonic('127.0.0.1', group, 'defaultServerIP');
            self.settingsList.addItemToGroupWithMnemonic(6665, group, 'defaultClientPort');
            self.settingsList.addItemToGroupWithMnemonic(6666, group, 'defaultServerPort');
            
            group = 'dotsAllObjectManagers';
            self.settingsList.addItemToGroupWithMnemonic('SnowDotsHost', group, 'hostName');
            
            group = 'dotsAllRemoteObjectManagers';
            self.settingsList.addItemToGroupWithMnemonic(true, group, 'clientMode');
            self.settingsList.addItemToGroupWithMnemonic(true, group, 'serverMode');
            self.settingsList.addItemToGroupWithMnemonic(1, group, 'transactionsTimeout');
            
            group = 'dotsTheDrawablesManager';
            self.settingsList.addItemToGroupWithMnemonic(true, group, 'clientMode');
            self.settingsList.addItemToGroupWithMnemonic(true, group, 'serverMode');
            self.settingsList.addItemToGroupWithMnemonic('127.0.0.1', group, 'clientIP');
            self.settingsList.addItemToGroupWithMnemonic('127.0.0.1', group, 'serverIP');
            self.settingsList.addItemToGroupWithMnemonic(49550, group, 'clientPort');
            self.settingsList.addItemToGroupWithMnemonic(49551, group, 'serverPort');
            
            group = 'dotsThePlayablesManager';
            self.settingsList.addItemToGroupWithMnemonic(true, group, 'clientMode');
            self.settingsList.addItemToGroupWithMnemonic(true, group, 'serverMode');
            self.settingsList.addItemToGroupWithMnemonic('127.0.0.1', group, 'clientIP');
            self.settingsList.addItemToGroupWithMnemonic('127.0.0.1', group, 'serverIP');
            self.settingsList.addItemToGroupWithMnemonic(49552, group, 'clientPort');
            self.settingsList.addItemToGroupWithMnemonic(49553, group, 'serverPort');
            
            group = 'dotsTheComputablesManager';
            self.settingsList.addItemToGroupWithMnemonic(true, group, 'clientMode');
            self.settingsList.addItemToGroupWithMnemonic(true, group, 'serverMode');
            self.settingsList.addItemToGroupWithMnemonic('127.0.0.1', group, 'clientIP');
            self.settingsList.addItemToGroupWithMnemonic('127.0.0.1', group, 'serverIP');
            self.settingsList.addItemToGroupWithMnemonic(49554, group, 'clientPort');
            self.settingsList.addItemToGroupWithMnemonic(49555, group, 'serverPort');
            self.settingsList.addItemToGroupWithMnemonic(inf, group, 'writeLogToDiskInterval');
            self.settingsList.addItemToGroupWithMnemonic('', group, 'writeLogToDiskFileName');
            
            group = 'dotsQueryableHIDMouse';
            matching = struct('PrimaryUsagePage', 1, 'PrimaryUsage', 2);
            self.settingsList.addItemToGroupWithMnemonic(matching, group, 'deviceMatching');
            matching = struct('UsagePage', 1, 'Usage', 48);
            self.settingsList.addItemToGroupWithMnemonic(matching, group, 'xMatching');
            matching = struct('UsagePage', 1, 'Usage', 49);
            self.settingsList.addItemToGroupWithMnemonic(matching, group, 'yMatching');
            matching = struct('UsagePage', 9);
            self.settingsList.addItemToGroupWithMnemonic(matching, group, 'buttonMatching');
            self.settingsList.addItemToGroupWithMnemonic(3, group, 'maxButtons');
            
            group = 'dotsQueryableHIDGamepad';
            matching = struct('PrimaryUsagePage', 1, 'PrimaryUsage', 5);
            self.settingsList.addItemToGroupWithMnemonic(matching, group, 'deviceMatching');
            matching = struct('UsagePage', 1, 'Usage', 48);
            self.settingsList.addItemToGroupWithMnemonic(matching, group, 'xMatching');
            matching = struct('UsagePage', 1, 'Usage', 49);
            self.settingsList.addItemToGroupWithMnemonic(matching, group, 'yMatching');
            matching = struct('UsagePage', 1, 'Usage', 50);
            self.settingsList.addItemToGroupWithMnemonic(matching, group, 'zMatching');
            matching = struct('UsagePage', 9);
            self.settingsList.addItemToGroupWithMnemonic(matching, group, 'buttonMatching');
            
            group = 'dotsQueryableHIDKeyboard';
            matching = struct('PrimaryUsagePage', 1, 'PrimaryUsage', 6);
            self.settingsList.addItemToGroupWithMnemonic(matching, group, 'deviceMatching');
            matching = struct('UsagePage', 7);
            self.settingsList.addItemToGroupWithMnemonic(matching, group, 'keyMatching');
            keysOfInterest = {'KeyboardF', 'KeyboardJ', ...
                'KeyboardEscape', 'KeyboardReturnOrEnter', 'KeyboardSpacebar', ...
                'KeyboardDownArrow', 'KeyboardLeftArrow', 'KeyboardRightArrow', 'KeyboardUpArrow'};
            self.settingsList.addItemToGroupWithMnemonic(keysOfInterest, group, 'keysOfInterest');
        end
        
        % Create an xml document object that contains the current settings.
        % @details
        % Returns a new com.mathworks.xml Java document object which
        % contains the settings from settingsList.  Each value is converted
        % to a string using the Snow Dots primitiveToString() utility.
        function xDoc = settingsToXmlDocument(self)
            xDoc = com.mathworks.xml.XMLUtils.createDocument('dotsMachineConfig');
            xRoot = xDoc.getDocumentElement;
            groups = self.settingsList.groups;
            for ii = 1:length(groups)
                groupNode = xDoc.createElement(groups{ii});
                [values, settings] = self.settingsList.getAllItemsFromGroup(groups{ii});
                for jj = 1:length(settings)
                    settingNode = xDoc.createElement(settings{jj});
                    settingNode.setTextContent(primitiveToString(values{jj}));
                    groupNode.appendChild(settingNode);
                end
                xRoot.appendChild(groupNode);
            end
        end
        
        % Read new settings from an xml document object.
        % @param xDoc a com.mathworks.xml Java document object which
        % contains settings values.
        % @details
        % Discards the current settings and populates settingsList with
        % values from @a xDoc.  Uses eval() to convert stored value strings
        % to Matlab variables.  @a xDoc should resemble the document
        % objects returned from settingsToXmlDocument().
        % @details
        % @a xDoc may be a partial list of settings.  Missing settings will
        % be filled in with default values from setDefaultConfiguration().
        function settingsFromXmlDocument(self, xDoc)
            self.setDefaultConfiguration;
            xRoot = xDoc.getDocumentElement;
            groupNode = xRoot.getFirstChild;
            while ~isempty(groupNode)
                groupName = char(groupNode.getNodeName);
                settingNode = groupNode.getFirstChild;
                while ~isempty(settingNode)
                    if settingNode.getNodeType == settingNode.ELEMENT_NODE
                        settingName = char(settingNode.getNodeName);
                        settingString = char(settingNode.getTextContent);
                        settingValue = eval(settingString);
                        self.settingsList.addItemToGroupWithMnemonic(settingValue, groupName, settingName);
                    end
                    settingNode = settingNode.getNextSibling;
                end
                groupNode = groupNode.getNextSibling;
            end
        end
        
        % Write an .xml file that contains the current settings.
        % @param fileWithPath the file name, which may contain a path,
        % where to write xml data.
        % @details
        % Writes an .xml file containing the current settings, at the given
        % @a fileWithPath.  Uses settingsToXmlDocument() to covert the
        % current settings to a com.mathworks.xml Java document object,
        % then writes the document object to file.
        % @details
        % Since the resulting .xml file contains human-readable strings,
        % the settings in it can be edited from any text editor.  The only
        % constraint is that the strings produce valid variables when
        % passed to Matlab's eval() function.
        % @details
        % The static methods
        % dotsTheMachineConfiguration.readUserSettingsFile() and
        % dotsTheMachineConfiguration.writeUserSettingsFile() may be more
        % convenient to use.
        function settingsToXmlFile(self, fileWithPath)
            if ischar(fileWithPath) && ~isempty(fileWithPath)
                xDoc = self.settingsToXmlDocument;
                xmlwrite(fileWithPath, xDoc);
                self.settingsFile = fileWithPath;
            end
        end
        
        % Read an .xml file that contains new settings.
        % @param fileWithPath the file name, which may contain a path,
        % where to find xml data.
        % @details
        % Reads an .xml file containing the new settings from the given @a
        % fileWithPath.  Uses settingsFromXmlDocument() to parse the file
        % then uses Matlab's eval() to convert stored strings to Matlab
        % variable and stores them in settingsList.  Discards any previous
        % settings.
        % @details
        % The static methods
        % dotsTheMachineConfiguration.readUserSettingsFile() and
        % dotsTheMachineConfiguration.writeUserSettingsFile() may be more
        % convenient to use.
        function settingsFromXmlFile(self, fileWithPath)
            if ischar(fileWithPath) && ~isempty(fileWithPath)
                xDoc = xmlread(fileWithPath);
                self.settingsFromXmlDocument(xDoc);
                self.settingsFile = fileWithPath;
            end
        end
    end
    
    methods (Static)
        % Access the current instance.
        function obj = theObject(varargin)
            persistent self
            if isempty(self) || ~isvalid(self)
                constructor = str2func(mfilename);
                self = feval(constructor, varargin{:});
            else
                self.set(varargin{:});
            end
            obj = self;
        end
        
        % Restore the current instance to a fresh state.
        function reset(varargin)
            factory = str2func([mfilename, '.theObject']);
            self = feval(factory, varargin{:});
            self.initialize;
        end
        
        % Launch a graphical interface to view and set settings.
        function g = gui
            self = dotsTheMachineConfiguration.theObject;
            
            yDiv = .1;
            xMargin = .2;
            listPosition = [0 yDiv, 1, 1-yDiv];
            buttonsPosition = [xMargin 0, 1-(2*xMargin), yDiv];
            
            g = topsGUI;
            listPanel = topsGroupedListPanel(g, listPosition);
            listPanel.groupString = 'class';
            listPanel.mnemonicString = 'property';
            listPanel.itemString = 'setting';
            listPanel.populateWithGroupedList(self.settingsList);
            listPanel.setEditable(true);
            showNewTitle(listPanel);
            
            buttons = { ...
                'save', {@saveSettings, listPanel, false}; ...
                'save as', {@saveSettings, listPanel, true}; ...
                'load', {@readAndRefreshPanel, listPanel}; ...
                'reset', {@resetAndRefreshPanel, listPanel}; ...
                };
            nButtons = size(buttons, 1);
            for ii = 1:nButtons
                pos = subposition(buttonsPosition, 1, nButtons, 1, ii);
                g.addButton(pos, buttons{ii,1}, buttons{ii,2});
            end
            
            function saveSettings(obj, event, listPanel, useDialog)
                self = dotsTheMachineConfiguration.theObject;
                if nargin < 4 || isempty(self.settingsFile)
                    useDialog = true;
                end
                
                showNewTitle(listPanel, 'saving...');
                if useDialog
                    dotsTheMachineConfiguration.writeUserSettingsFile;
                else
                    dotsTheMachineConfiguration.writeUserSettingsFile( ...
                        self.settingsFile);
                end
                showNewTitle(listPanel);
            end
            
            function readAndRefreshPanel(obj, event, listPanel)
                showNewTitle(listPanel, 'loading...');
                listPanel.unpopulate;
                dotsTheMachineConfiguration.readUserSettingsFile;
                self = dotsTheMachineConfiguration.theObject;
                listPanel.populateWithGroupedList(self.settingsList);
                showNewTitle(listPanel);
            end
            
            function resetAndRefreshPanel(obj, event, listPanel)
                showNewTitle(listPanel, 'resetting...');
                listPanel.unpopulate;
                dotsTheMachineConfiguration.reset;
                self = dotsTheMachineConfiguration.theObject;
                listPanel.populateWithGroupedList(self.settingsList);
                showNewTitle(listPanel);
            end
            
            function showNewTitle(listPanel, title)
                if nargin < 2
                    self = dotsTheMachineConfiguration.theObject;
                    fileName = self.settingsFile;
                    if isempty(fileName)
                        title = 'no settings file loaded';
                    else
                        title = self.settingsFile;
                    end
                end
                listPanel.parentGUI.title = title;
                drawnow
            end
        end
        
        % Get a correctly formatted "host settings" file name.
        % @details
        % Returns a file name convenient for writing an .xml settings file.
        % If dotsTheMachineConfiguration finds a file with this name on the
        % Matlab path, it will automatically load settings from it.
        function hostFile = getHostSettingsFilename
            if isunix
                [stat,h] = unix('hostname -s');
            else
                h = 'windows';
            end
            hostFile = sprintf('dots_%s_MachineConfig.xml', deblank(h));
        end
        
        % Get settings values for a given class.
        % @param className the name of a class (or other group) to get
        % settings for.
        % @details
        % Returns a cell array containing all the settings values stored in
        % the @a className group of settingsList.  Also returns, as an
        % optional second argument, a cell array of strings with the
        % string mnemonic for each returned value.
        % @details
        % If @a className is not the name of a group in settingsList,
        % returns {}.
        function [vals, props] = getSettingsForClass(className)
            self = dotsTheMachineConfiguration.theObject;
            if self.settingsList.containsGroup(className)
                [vals, props] = ...
                    self.settingsList.getAllItemsFromGroup(className);
            else
                vals = {};
                props = {};
            end
        end
        
        % Assign defaults to an object.
        % @param object an object to receive default property settings
        % @param withSupers true or false (default), whether to also apply
        % settings listed for the superclasses of @a object
        % @details
        % Uses the class name of @a object to look up any settings values.
        % If any settings are listed for that class, applies each
        % setting value to the same-named property of @a object.
        % @details
        % If @a withSupers is true, also applies any settings listed for
        % the superclasses of @a object.  Applies superclass settings in
        % descending order, ending with @a object's own class settings.
        % @details
        % If no settings are listed for the class of @object, returns
        % silently.  Thus, any object may request machine settings with
        % this method.
        function applySettingsToObject(object, withSupers)
            if nargin > 1 && withSupers
                superclassNames = superclasses(object);
                for ii = length(superclassNames):-1:1
                    dotsTheMachineConfiguration.applySettingsToObjectAsClass( ...
                        object, superclassNames{ii});
                end
            end
            dotsTheMachineConfiguration.applySettingsToObjectAsClass( ...
                object, class(object));
        end
        
        % Assign defaults to an object, ignoring its class.
        % @param object an object to apply settings to.
        % @param className the name of a class as which to treat @a object.
        % @details
        % Uses the @a className to look up any settings values.
        % If any settings are listed for that class, applies each
        % setting value to the same-named property of @a object.
        % @details
        % @a className should be the class name of @a object, one of its
        % superclasses, or the name of a settings group whose string
        % mnemonics match the properties of @a object.
        function applySettingsToObjectAsClass(object, className)
            [setVals, setProps] = ...
                dotsTheMachineConfiguration.getSettingsForClass(className);
            
            classProps = properties(object);
            [validProps, validIndexes] = ...
                intersect(setProps, classProps);
            for ii = 1:length(validIndexes)
                index = validIndexes(ii);
                object.(setProps{index}) = setVals{index};
            end
        end
        
        % Read an .xml file that contains new settings.
        % @param fileWithPath the file name, which may contain a path,
        % where to find xml data.
        % @details
        % If @a fileWithPath is missing, opens a dialog for chosing a
        % suitable file.
        function readUserSettingsFile(fileWithPath)
            self = dotsTheMachineConfiguration.theObject;
            
            if nargin < 1 || isempty(fileWithPath) || ~ischar(fileWithPath)
                if exist(self.settingsFile, 'file')
                    [p,f,e] = fileparts(which(self.settingsFile));
                    if isempty(p)
                        p = pwd;
                    end
                    suggestion = fullfile(p, self.settingsFile);
                else
                    suggestion = fullfile(pwd, '*');
                end
                
                [f, p] = uigetfile( ...
                    {'*.xml'}, ...
                    'Load dots settings from which .xml file?', ...
                    suggestion, ...
                    'MultiSelect', 'off');
                
                if ischar(f)
                    fileWithPath = fullfile(p, f);
                else
                    return;
                end
            end
            self.settingsFromXmlFile(fileWithPath)
        end
        
        % Write an .xml file that contains the current settings.
        % @param fileWithPath the file name, which may contain a path,
        % where to write xml data.
        % @details
        % If @a fileWithPath is missing, opens a dialog for chosing a
        % suitable file.
        function writeUserSettingsFile(fileWithPath)
            self = dotsTheMachineConfiguration.theObject;
            
            if nargin < 1 || isempty(fileWithPath) || ~ischar(fileWithPath)
                if exist(self.settingsFile, 'file')
                    [p,f,e] = fileparts(which(self.settingsFile));
                    if isempty(p)
                        p = pwd;
                    end
                    suggestion = fullfile(p, self.settingsFile);
                else
                    hf = self.getHostSettingsFilename;
                    suggestion = fullfile(pwd, hf);
                end
                
                [f, p] = uiputfile( ...
                    {'*.xml'}, ...
                    'Save dots settings to which .xml file?', ...
                    suggestion);
                
                if ischar(f)
                    fileWithPath = fullfile(p, f);
                else
                    return;
                end
            end
            self.settingsToXmlFile(fileWithPath)
        end
    end
end